﻿using System;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
using FirstFloor.Documents.Controls;

namespace FirstFloor.Documents.Presentation.Controls
{
    /// <summary>
    /// A document viewer with zooming and panning functionality similar to MultiScaleImage.
    /// </summary>
    public class ScaleDocumentViewer
        : Control
    {
        /// <summary>
        /// Identifies the DocumentDataSource dependency property.
        /// </summary>
        public static readonly DependencyProperty DocumentDataSourceProperty = DependencyProperty.Register("DocumentDataSource", typeof(DocumentDataSource), typeof(ScaleDocumentViewer), new PropertyMetadata(new PropertyChangedCallback(OnDocumentDataSourceChanged)));
        /// <summary>
        /// Identifies the PageIndex dependency property.
        /// </summary>
        public static readonly DependencyProperty PageIndexProperty = DependencyProperty.Register("PageIndex", typeof(int), typeof(ScaleDocumentViewer), new PropertyMetadata(-1, OnPageIndexChanged));
        /// <summary>
        /// Identifies the PageCount dependency property.
        /// </summary>
        public static readonly DependencyProperty PageCountProperty = DependencyProperty.Register("PageCount", typeof(int), typeof(ScaleDocumentViewer), new PropertyMetadata(0));
        /// <summary>
        /// Identifies the AspectRatio dependency property.
        /// </summary>
        public static readonly DependencyProperty AspectRatioProperty = DependencyProperty.Register("AspectRatio", typeof(double), typeof(ScaleDocumentViewer), new PropertyMetadata(1D, OnAspectRatioChanged));
        /// <summary>
        /// Identifies the UseSprings dependency property.
        /// </summary>
        public static readonly DependencyProperty UseSpringsProperty = DependencyProperty.Register("UseSprings", typeof(bool), typeof(ScaleDocumentViewer), new PropertyMetadata(true, OnUseSpringsChanged));
        /// <summary>
        /// Identifies the ViewportWidth dependency property.
        /// </summary>
        public static readonly DependencyProperty ViewportWidthProperty = DependencyProperty.Register("ViewportWidth", typeof(double), typeof(ScaleDocumentViewer), new PropertyMetadata(1D, OnViewportWidthChanged));
        /// <summary>
        /// Identifies the ViewportOrigin dependency property.
        /// </summary>
        public static readonly DependencyProperty ViewportOriginProperty = DependencyProperty.Register("ViewportOrigin", typeof(Point), typeof(ScaleDocumentViewer), new PropertyMetadata(new Point(), OnViewportOriginChanged));

        /// <summary>
        /// Occurs when the viewport (the area of the document displayed) changes.
        /// </summary>
        public event EventHandler ViewportChanged;

        private Canvas panel;
        private FrameworkElement pageContent;
        private Storyboard pageLoadStoryboard;
        private Storyboard springStoryboard;
        private DoubleAnimation pageX;
        private DoubleAnimation pageY;
        private DoubleAnimation pageWidth;
        private DoubleAnimation pageHeight;
        private VirtualizingPageCollection pages;

        /// <summary>
        /// Initializes a new instance of the <see cref="ScaleDocumentViewer"/> class.
        /// </summary>
        public ScaleDocumentViewer()
        {
            this.DefaultStyleKey = typeof(ScaleDocumentViewer);
        }

        private static void OnDocumentDataSourceChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
        {
            ((ScaleDocumentViewer)o).OnDocumentDataSourceChanged((DocumentDataSource)e.OldValue, (DocumentDataSource)e.NewValue);
        }

        private void OnDocumentDataSourceChanged(DocumentDataSource oldValue, DocumentDataSource newValue)
        {
            if (this.pages != null) {
                this.pages.CollectionChanged -= OnPagesCollectionChanged;
                this.pages = null;
            }
            if (newValue != null) {
                this.pages = newValue.CreatePageCollection();
                this.pages.CollectionChanged += OnPagesCollectionChanged;
            }
        }

        private static void OnPageIndexChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
        {
            ((ScaleDocumentViewer)o).OnPageIndexChanged((int)e.OldValue, (int)e.NewValue);
        }

        private void OnPageIndexChanged(int oldValue, int newValue)
        {
            if (this.PageCount == 0) {
                this.PageIndex = -1;
            }
            else {
                // unload previous page
                var oldPage = this.pages.ElementAtOrDefault(oldValue);
                if (oldPage != null) {
                    oldPage.PropertyChanged -= OnPagePropertyChanged;
                    oldPage.IsInUse = false;
                }

                var page = this.pages.ElementAtOrDefault(newValue);
                if (page == null) {
                    // reset
                    this.PageIndex = oldValue;
                }
                else {
                    page.PropertyChanged += OnPagePropertyChanged;

                    // load new page
                    page.IsInUse = true;

                    // update aspect ratio
                    this.AspectRatio = page.FixedPageSize.Width / page.FixedPageSize.Height;
                }

                if (this.pageContent != null) {
                    this.pageContent.DataContext = page;
                }
            }
        }

        private void OnPagePropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            var page = (PageContextBase)sender;
            if (e.PropertyName == null || e.PropertyName == "FixedPage" || e.PropertyName == "FixedPageSize") {
                // ensure aspect ratio is current
                this.AspectRatio = page.FixedPageSize.Width / page.FixedPageSize.Height;

                // fade in page
                if (this.pageLoadStoryboard != null && e.PropertyName == "FixedPage" && page.FixedPage != null) {
                    this.pageLoadStoryboard.Begin();
                }

                DoLayout();
            }
        }

        private static void OnAspectRatioChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
        {
            ((ScaleDocumentViewer)o).DoLayout();
        }

        private static void OnUseSpringsChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
        {
            ((ScaleDocumentViewer)o).OnUseSpringsChanged((bool)e.NewValue);
        }

        private void OnUseSpringsChanged(bool newValue)
        {
            if (this.pageX != null) {
                var duration = new Duration(newValue ? TimeSpan.FromMilliseconds(700) : TimeSpan.Zero);

                this.pageX.Duration = duration;
                this.pageY.Duration = duration;
                this.pageWidth.Duration = duration;
                this.pageHeight.Duration = duration;
            }
        }

        private static void OnViewportWidthChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
        {
            ((ScaleDocumentViewer)o).DoLayout();
        }

        private static void OnViewportOriginChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
        {
            ((ScaleDocumentViewer)o).DoLayout();
        }

        private void OnPagesCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            this.PageCount = this.pages.Count();
            this.PageIndex = this.PageCount > 0 ? 0 : -1;
        }

        /// <summary>
        /// When overridden in a derived class, is invoked whenever application code or internal processes (such as a rebuilding layout pass) call <see cref="M:System.Windows.Controls.Control.ApplyTemplate"/>. In simplest terms, this means the method is called just before a UI element displays in an application. For more information, see Remarks.
        /// </summary>
        public override void OnApplyTemplate()
        {
            this.panel = (Canvas)GetTemplateChild("Panel");
            this.pageContent = (FrameworkElement)GetTemplateChild("PageContent");

            this.springStoryboard = (Storyboard)GetTemplateChild("SpringStoryboard");
            this.pageLoadStoryboard = (Storyboard)GetTemplateChild("PageLoadStoryboard");
            this.pageX = (DoubleAnimation)GetTemplateChild("PageX");
            this.pageY = (DoubleAnimation)GetTemplateChild("PageY");
            this.pageWidth = (DoubleAnimation)GetTemplateChild("PageWidth");
            this.pageHeight = (DoubleAnimation)GetTemplateChild("PageHeight");

            this.panel.SizeChanged += OnLayoutRootSizeChanged;

            OnUseSpringsChanged(this.UseSprings);

            DoLayout();
        }

        private void OnViewportChanged()
        {
            if (ViewportChanged != null) {
                ViewportChanged(this, EventArgs.Empty);
            }
        }

        private void OnLayoutRootSizeChanged(object sender, SizeChangedEventArgs e)
        {
            DoLayout();

            OnViewportChanged();
        }

        private void DoLayout()
        {
            if (this.panel != null) {
                this.panel.Clip = new RectangleGeometry { Rect = new Rect(0, 0, this.panel.ActualWidth, this.panel.ActualHeight) };
                var width = this.panel.ActualWidth / this.ViewportWidth;
                var height = width / this.AspectRatio;
                var x = -this.ViewportOrigin.X * width;
                var y = -this.ViewportOrigin.Y * width;

                this.pageX.To = x;
                this.pageY.To = y;
                this.pageWidth.To = width;
                this.pageHeight.To = height;

                this.springStoryboard.Begin();

                //Debug.WriteLine("{0:0}x{1:0}", width, height);
            }
        }

        /// <summary>
        /// Gets the aspect ratio of the current page.
        /// </summary>
        /// <value>The aspect ratio.</value>
        public double AspectRatio
        {
            get { return (double)GetValue(AspectRatioProperty); }
            private set { SetValue(AspectRatioProperty, value); }
        }

        /// <summary>
        /// Gets or sets the document data source.
        /// </summary>
        /// <value>The document data source.</value>
        public DocumentDataSource DocumentDataSource
        {
            get { return (DocumentDataSource)GetValue(DocumentDataSourceProperty); }
            set { SetValue(DocumentDataSourceProperty, value); }
        }

        /// <summary>
        /// Gets or sets the 0 based index of the current page.
        /// </summary>
        /// <value>The index of the page.</value>
        /// <remarks>
        /// <para>Page index -1 is used to identify there is no current page</para>
        /// <para>Valid page index values range from -1 to <see cref="PageCount"/> minus 1.</para>
        /// </remarks>
        public int PageIndex
        {
            get { return (int)GetValue(PageIndexProperty); }
            set { SetValue(PageIndexProperty, value); }
        }

        /// <summary>
        /// Gets the page count of the current document.
        /// </summary>
        /// <value>The page count.</value>
        public int PageCount
        {
            get { return (int)GetValue(PageCountProperty); }
            private set { SetValue(PageCountProperty, value); }
        }

        /// <summary>
        /// Gets or sets a value that indicates whether the control uses spring animations.
        /// </summary>
        /// <value><c>true</c> if spring animations are used; otherwise, <c>false</c>.</value>
        public bool UseSprings
        {
            get { return (bool)GetValue(UseSpringsProperty); }
            set { SetValue(UseSpringsProperty, value); }
        }

        /// <summary>
        /// Gets or sets the width of the viewport.
        /// </summary>
        /// <value>The width of the viewport.</value>
        public double ViewportWidth
        {
            get { return (double)GetValue(ViewportWidthProperty); }
            set { SetValue(ViewportWidthProperty, value); }
        }

        /// <summary>
        /// Gets or sets the viewport origin.
        /// </summary>
        /// <value>The viewport origin.</value>
        public Point ViewportOrigin
        {
            get { return (Point)GetValue(ViewportOriginProperty); }
            set { SetValue(ViewportOriginProperty, value); }
        }

        /// <summary>
        /// Gets a point with logical coordinates (values between 0 and 1) from a point of the PanAndZoomContainer.
        /// </summary>
        /// <param name="elementPoint">The element point.</param>
        /// <returns></returns>
        public Point ElementToLogicalPoint(Point elementPoint)
        {
            var x = this.pageX.To ?? 0;
            var y = this.pageY.To ?? 0;
            var width = this.pageWidth.To ?? 0;

            return new Point((elementPoint.X - x) / width, (elementPoint.Y - y) / width);
        }

        /// <summary>
        /// Gets a point with pixel coordinates relative to the PanAndZoomContainer from a logical point (values between 0 and 1).
        /// </summary>
        /// <param name="logicalPoint">The logical point.</param>
        /// <returns></returns>
        public Point LogicalToElementPoint(Point logicalPoint)
        {
            var x = this.pageX.To ?? 0;
            var y = this.pageY.To ?? 0;
            var width = this.pageWidth.To ?? 0;

            return new Point(logicalPoint.X * width + x, logicalPoint.Y * width + y);
        }

        /// <summary>
        /// Enables a user to zoom in on a point of the PanAndZoomContainer.
        /// </summary>
        /// <param name="zoomIncrementFactor">The zoom increment factor.</param>
        /// <param name="zoomCenterLogicalX">The zoom center logical X.</param>
        /// <param name="zoomCenterLogicalY">The zoom center logical Y.</param>
        public void ZoomAboutLogicalPoint(double zoomIncrementFactor, double zoomCenterLogicalX, double zoomCenterLogicalY)
        {
            if (zoomIncrementFactor <= 0) {
                throw new ArgumentOutOfRangeException("zoomIncrementFactor");
            }

            var newViewPortWidth = this.ViewportWidth / zoomIncrementFactor;
            var vector = newViewPortWidth / this.ViewportWidth - 1;

            var x = this.ViewportOrigin.X / zoomIncrementFactor - zoomCenterLogicalX * vector;
            var y = this.ViewportOrigin.Y / zoomIncrementFactor - zoomCenterLogicalY * vector;

            this.ViewportWidth = newViewPortWidth;
            this.ViewportOrigin = new Point(x, y);
        }
    }
}
